package boardgamer.common;

import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;

import java.io.Serializable;
import java.util.Iterator;
import java.util.List;

import static com.google.common.base.Preconditions.checkState;


public class CyclicIterator<E> implements Iterator<E>, Serializable {

  private static final long serialVersionUID = -2930658906587885302L;
  
  private final List<E> list;
  private final SynchronizedObject<Integer> currentIndex;
  private final SynchronizedObject<Boolean> hasCycled;

  public static <E> CyclicIterator<E> copyOf(CyclicIterator<E> iterator) {
    CyclicIterator<E> newIterator;
    synchronized (iterator.list) {
      synchronized (iterator.currentIndex) {
        newIterator = new CyclicIterator<E>(Lists.newArrayList(iterator.list));
        newIterator.currentIndex.set(iterator.currentIndex.get());
        newIterator.hasCycled.set(iterator.hasCycled.get());
      }
    }

    return newIterator;
  }

  public CyclicIterator() {
    this(ImmutableList.<E>of());
  }

  public CyclicIterator(List<E> list) {
    this.list = Lists.newArrayList(list);
    this.currentIndex = new SynchronizedObject<Integer>(0);
    this.hasCycled = new SynchronizedObject<Boolean>(false);
  }

  @Override
  public boolean hasNext() {
    boolean hasNext;
    synchronized (list) {
      hasNext = !list.isEmpty();
    }
    return hasNext;
  }

  @Override
  public E next() {
    E next;
    synchronized (list) {
      synchronized (currentIndex) {
        synchronized (hasCycled) {
          checkState(hasNext(), "There is no next element");
          int nextIndex = currentIndex.get() + 1;
          if (nextIndex >= list.size()) {
            nextIndex -= list.size();
            hasCycled.set(true);
          }

          currentIndex.set(nextIndex);
          next = peek();
        }
      }
    }
    return next;
  }

  public E peek() {
    E peek;
    synchronized (list) {
      synchronized (currentIndex) {
        peek = list.get(currentIndex.get());
      }
    }
    return peek;
  }

  public int size() {
    int size;
    synchronized (list) {
      size = list.size();
    }
    return size;
  }
  
  public List<E> asList() {
    List<E> toReturn;
    synchronized (list) {
      toReturn = ImmutableList.copyOf(list);
    }
    return toReturn;
  }

  @Override
  public void remove() {
    synchronized (list) {
      synchronized (currentIndex) {
        int current = currentIndex.get();
        list.remove(current);
        if (current >= list.size()) {
          current -= list.size();
        }
        currentIndex.set(current);
      }
    }
  }

  public void removeFirstOccurence(E player) {
    synchronized (list) {
      synchronized (currentIndex) {
        int toRemove = list.indexOf(player);
        if (toRemove >= 0) {
          if (toRemove < currentIndex.get()) {
            currentIndex.set(currentIndex.get() - 1);
          }
          list.remove(toRemove);
        }
      }
    }
  }

  @SuppressWarnings("unchecked")
  @Override
  public boolean equals(Object obj) {
    boolean equals;
    if (!(obj instanceof CyclicIterator<?>)) {
      return false;
    }
    synchronized (list) {
      synchronized (currentIndex) {
        synchronized (hasCycled) {
          CyclicIterator<E> that = (CyclicIterator<E>) obj;
          equals = Objects.equal(this.list, that.list)
              && Objects.equal(this.currentIndex, that.currentIndex)
              && Objects.equal(this.hasCycled, that.hasCycled);
        }
      }
    }

    return equals;
  }

  @Override
  public int hashCode() {
    int hashCode;
    synchronized (list) {
      synchronized (currentIndex) {
        synchronized (hasCycled) {
          hashCode = Objects.hashCode(list, currentIndex, hasCycled);
        }
      }
    }
    return hashCode;
  }

  @Override
  public String toString() {
    String toString;
    synchronized (list) {
      synchronized (currentIndex) {
        synchronized (hasCycled) {
          toString = Objects.toStringHelper(this)
              .add("list", list)
              .add("currentIndex", currentIndex)
              .add("hasCycled", hasCycled)
              .toString();
        }
      }
    }
    return toString;
  }

  public boolean contains(E e) {
    boolean rv;
    synchronized (list) {
      rv = list.contains(e);
    }
    return rv;
  }

  public boolean hasCycled() {
    boolean rv;
    synchronized (hasCycled) {
      rv = hasCycled.get();
    }
    return rv;
  }
}
